﻿using System;
using System.Collections.Generic;
using System.Text;

namespace SqlCeDriver
{
	/*
		Generates code for use by EF under various configurations
		Sections
		The Class
		The Context
		Fluent configuration to include in context override method
		Fluent configuration class
		Sample code
		module template

		Scope
		1. Full: combine all
		2. Class only. Class + fluent config
		3. Class and sample. Class + fluent config + Sample

		Todo:
		Try code on a variety of tables.
		Can't cope:
			- sql variant (EF limit)
			- fluent needs PKEY configuration

		* Distinguish between fluent types.
		Order code gen
		* Template dictionary?
		handle the indents
			- tabs + detabber?
		- other classes all options,

	*/
	internal class EFClassGenerator
	{
		public EFClassGenerator(String className, String tableName, SqlTableSchema schema)
		{
			this.className = className;
			this.sourceTableName = tableName;
			this.schema = schema;
		}

		internal enum CodeConfig {Annotations, FluentCentral, FluentIndividual}

		public String GenerateCode(String connectionString, int scope, CodeConfig cfgOption)
		{
			// allow for schema 
			String[] tableParts = Helper.ParseTableName(this.sourceTableName);

			String tableName = tableParts[0];
			String schemaName = (tableParts.GetLength(0) == 2) ? tableParts[1] : "dbo";
			String className = (this.className.Length > 0) ? this.className 
														  : TableGeneratorHelpers.MakePascalCaseName(tableName, true);
			
			DbView.FieldInfo fi = this.schema[0];
			
			// create an object to hold all the linked bits of template
			TemplateMap map = new TemplateMap();
			// populate the template
			map.Add("TABLE", tableName);
			map.Add("CLASS", className);   // name of the class
			map.Add("CLASSDEF", CLASS_DEF);  // the code that defines the class
			map.Add("CLASSFRAME", CLASS_FRAME);
			map.Add("DBSET", DBSET_TEMPLATE);

			// POCO class properties from the table columns.
			map.Add("PROPERTIES", this.GenerateProperties(className, cfgOption)); // 

			// this tag depends on the fluent variation
			String entity = (cfgOption == CodeConfig.FluentCentral) ? "modelBuilder.Entity<%%CLASS%%>()" : 
							(cfgOption == CodeConfig.FluentIndividual) ? "this" : "";
			map.Add("ENTITY", entity); // 
			map.Add("TABLEANNOTATION", this.TableAnnotation(className, cfgOption)); // 
			// sample code
			map.Add("SAMPLECODE", SAMPLE_TEMPLATE);
			map.Add("SAMPLEFRAME", SAMPLE_FRAME);
			map.Add("FIELD1", TableGeneratorHelpers.MakePascalCaseName(fi.Name)); // first field for sample data
			map.Add("CONNSTR", connectionString.Replace("\\", "\\\\"));				// sample code requires the current connection to make working code
			// connect the config in the context class with the stand-alone config class 
			// only for FluentIndividual option
			String contextFluent = (cfgOption == CodeConfig.FluentCentral) ? this.GenerateFluentCode(className, cfgOption) :
									(cfgOption == CodeConfig.FluentIndividual) ? USE_ENTITY_CONFIGURATION_TEMPLATE : "";
			map.Add("CONTEXTFLUENT", contextFluent);
			String configFluent = (cfgOption == CodeConfig.FluentIndividual) ? this.GenerateFluentCode(className, cfgOption) : "";
			map.Add("CONFIGFLUENT",configFluent);
			map.Add("CONFIGCLASS", (cfgOption == CodeConfig.FluentIndividual) ? ENTITY_CONFIGURATION_TEMPLATE : "");
			
			map.Add("CONTEXT", CONTEXT_TEMPLATE);
			map.Add("MODULE", MODULE_TEMPLATE);
			// extra usings added or not.
			map.Add("USINGANNOTATIONS", (cfgOption == CodeConfig.Annotations) ? "\r\nusing System.ComponentModel.DataAnnotations;" : "");
			map.Add("USINGFLUENT", (cfgOption == CodeConfig.Annotations) ? "" : "\r\nusing System.Data.Entity.ModelConfiguration;");

			// A different frame for the scope of code to generate.
			switch (scope)
			{
				case 0: return TemplateMap.Resolve("CLASSFRAME", map, false);
				case 1: return TemplateMap.Resolve("SAMPLEFRAME",  map, false);
				case 2: return TemplateMap.Resolve("MODULE",  map, false);
			}
			// apply all templates.
			return TemplateMap.Resolve("CLASSFRAME", map, false);
		}

		// Data Annotations option only Map the table name to the class name
		String TableAnnotation(String className, CodeConfig cfgOption)
		{
			String[] tableParts = Helper.ParseTableName(this.sourceTableName);
			String tableName = tableParts[0];
			String tableAnnotation = "";
			if (cfgOption != CodeConfig.Annotations) return tableAnnotation;
			if (tableParts.GetLength(0) > 1) // allow for schema
			{
				tableAnnotation = String.Format("[Table(\"{0}\", Schema=\"{1}\")]", tableParts[0], tableParts[1]);
			}
			else
			{
				tableAnnotation = String.Format("[Table(\"{0}\")]", tableParts[0]);
			}
			tableAnnotation = "	" + tableAnnotation + "\r\n";
			return tableAnnotation;
		}

		public String GenerateProperties(String className, CodeConfig cfgOption)
		{
			List<String> codeLines = new List<String>();  // holds target Code 
			//String[] tableParts = Helper.ParseTableName(this.m_SourceTableName);
			//String tableName = tableParts[0];
			// 0 = name 1 = type 2 = atts
			String columnTemplate =
@"{2}		public {1} {0} {{get; set;}}
";
			int keyCount = 0; // key order for data annotations
			for (int idx = 0; idx < this.schema.Count; ++idx)
			{
				DbView.FieldInfo fi = this.schema[idx];
				String columnAnnotation = (cfgOption == CodeConfig.Annotations) ? this.MakeColumnParams(fi, keyCount) : "";
				// align
				if (columnAnnotation.Length > 0)
					columnAnnotation = "		"+columnAnnotation+"\r\n";
				String line = String.Format(columnTemplate, 
											TableGeneratorHelpers.MakePascalCaseName(fi.Name), 
											TableGeneratorHelpers.MakeTypeString(fi), 
											columnAnnotation);
				codeLines.Add(line);
				if (fi.IsPKey) ++ keyCount; // for following key fields.
			}

			return String.Join("", codeLines.ToArray());
		}

		public String GenerateFluentCode(String className, CodeConfig cfgOption)
		{
			// no fluent config if annotations
			if (cfgOption == CodeConfig.Annotations) return "";
			
			// table
			String[] tableParts = Helper.ParseTableName(this.sourceTableName);
			String tableName = tableParts[0];
			String schemaName = (tableParts.GetLength(0) == 2) ? tableParts[1] : "dbo";
			// Collection property of DbContext class
			String code = String.Format("			// Add to DbContext.OnModelCreating() Configuration for {0}\r\n", className);
			code += String.Format("			%%ENTITY%%.ToTable(\"{0}\",\"{1}\");\r\n", tableName, schemaName);

			List<String> codeLines = new List<String>();  // holds target Code 
			List<String> keys = new List<String>();  // holds key list

			// 0 = classname 1 = propertyname 2 = column name
			String fluent1ColumnTemplate = "			%%ENTITY%%.Property(p => p.{0}).HasColumnName(\"{1}\");";
			String fluent1KeyTemplate	 = "			%%ENTITY%%.HasKey( k => new {{k.{0}}} );";

			for (int idx = 0; idx < this.schema.Count; ++idx)
			{
				DbView.FieldInfo fi = this.schema[idx];
				String propertyName = TableGeneratorHelpers.MakePascalCaseName(fi.Name);
				if (propertyName != fi.Name)
					codeLines.Add(String.Format(fluent1ColumnTemplate, propertyName, fi.Name));
				if (fi.IsPKey)
					keys.Add(propertyName);
			}
			if (keys.Count > 0)
				codeLines.Add(String.Format(fluent1KeyTemplate, String.Join(", k.", keys.ToArray())));

			code = code + String.Join("\r\n", codeLines.ToArray());
			return code;
		}

		/*
		1. Make all lower
		2. Capitalise 1st
		3. Capitalise all after _ space - 
		4. Remove _ space -
		*/

		private string MakeColumnParams(DbView.FieldInfo fi, int keyOrder)
		{
			List<String> columnParms = new List<String>();
			
			if (fi.IsPKey)
				columnParms.Add("Key");

			List<String> columnData = new List<String>();
			if (fi.Name != TableGeneratorHelpers.MakePascalCaseName(fi.Name))
				columnData.Add("\""+fi.Name+"\"");
			if (fi.IsPKey)
				columnData.Add("Order=" + keyOrder.ToString());
			
			if (columnData.Count > 0)
			{
				columnParms.Add("Column("+String.Join(", ", columnData.ToArray())+")");
			}
			if (fi.Type == "C")
				columnParms.Add("MaxLength(" + fi.Length.ToString() + ")");
			if (!fi.AllowsNulls)
				columnParms.Add("Required");
			if (columnParms.Count == 0) return ""; 
			return "["+String.Join(", ", columnParms.ToArray())+"]";
		}

		String className;
		String sourceTableName;
		SqlTableSchema schema;

		const String MODULE_TEMPLATE = @"
using System;
using System.Linq;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;%%USINGANNOTATIONS%%%%USINGFLUENT%%

namespace CodeGenTest
{
	class Program
	{
		///////////////////////////////////////////////////////////////
		// Test code 
		static void Main(string[] args)
		{
			// nb: EF does not work with SQL CE 3.5
			Database.DefaultConnectionFactory = new SqlCeConnectionFactory(""System.Data.SqlServerCe.4.0"");
			TestClass(""%%CONNSTR%%"");
		}
%%SAMPLECODE%%
	}
	///////////////////////////////////////////////////////////////
	// Database Context
%%CONTEXT%%
	///////////////////////////////////////////////////////////////
	// Generated class
%%CLASSDEF%%
}
";
		const String SAMPLE_TEMPLATE = @"
		private static void TestClass(String connectionString)
		{
			%%CLASS%%DbContext db = new %%CLASS%%DbContext(connectionString);
			var result = (from o%%CLASS%% in db.%%CLASS%%s
						select o%%CLASS%%).Take(5);
			foreach (%%CLASS%% o%%CLASS%% in result)
			{
				Console.WriteLine(""{0}"", o%%CLASS%%.%%FIELD1%%);
			}
		}
";

		const String DBSET_TEMPLATE=@"		public DbSet<%%CLASS%%> %%CLASS%%s {get; set; }";

		const String CONTEXT_TEMPLATE = @"
	public class %%CLASS%%DbContext : DbContext 
	{
		/// <summary>
		/// Constructor with connection
		/// </summary>
		public %%CLASS%%DbContext(String connStr) : base(connStr)
		{
		}
%%DBSET%%
		protected override void OnModelCreating(DbModelBuilder modelBuilder)
		{
			// add fluent configuration here.
%%CONTEXTFLUENT%%
		}
	}
%%CONFIGCLASS%%
";
		const String ENTITY_CONFIGURATION_TEMPLATE=@"
	public class %%CLASS%%Configuration: EntityTypeConfiguration<%%CLASS%%>
	{
		public %%CLASS%%Configuration()
		{
%%CONFIGFLUENT%%
		}
	}
";
		const String CLASS_DEF = 
@"%%TABLEANNOTATION%%	public class %%CLASS%%
	{
%%PROPERTIES%%
	}
";
		const String SAMPLE_FRAME = 
@"%%SAMPLECODE%%
%%CLASSFRAME%%
";

		const String CLASS_FRAME =
@"		// Add to DbContext class Collection property of POCO class
%%DBSET%%

%%CONTEXTFLUENT%%
%%CONFIGCLASS%%
%%CLASSDEF%%
";
		const String USE_ENTITY_CONFIGURATION_TEMPLATE=@"			modelBuilder.Configurations.Add(new %%CLASS%%Configuration());";
	}
}
